本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
連續跟函式交手了這麼多天,今天終於要來與各位介紹到 JavaScript 最容易被誤解排行榜第一名的 this
了。 (是說哪來排行榜)
在鐵人開賽之前,我曾經針對 this
寫下三篇介紹文,相信也許有些朋友也看過了:
本來想說靠這三篇可以度過愉快的三天鐵人,不過想想要是直接原封不動就貼上鐵人賽,這跟作弊沒什麼兩樣。 XD
但因為 this
的觀念在 JavaScript 實在太重要,也不可能完全不提到,所以我打算利用今天這一篇,將上面三篇文章做個濃縮精華版,再次與各位介紹 this
是什麼東西。
就連美國隊長都從第一集 do this
do 到第三集,誠心希望他看完這篇可以順利搞懂 this
。 (超大誤)
也許你在其他物件導向的程式語言曾經看過 this
,也知道它會指向某個建構子 (constructor) 所建立的物件。 但事實上在 JavaScript 裡面, this
所代表的不僅僅是那個被建立的物件。
先來看看 ECMAScript 標準規範 對 this 的定義:
「The this keyword evaluates to the value of the ThisBinding of the current execution context.」
「this 這個關鍵字代表的值為目前執行環境的 ThisBinding。」
然後來看看 MDN 對 this 的定義:
「In most cases, the value of this is determined by how a function is called.」
「在大多數的情況下,this 會因為 function 被呼叫的方式而有所不同。」
好,如果上面兩行就看得懂的話那麼就不用再往下看了,恭喜你。
...... 我想應該不會,至少我光看這兩行還是不懂。
所以,this
到底是什麼?
this
是 JavaScript 的一個關鍵字。this
是 function 執行時,自動生成的一個內部物件。this
所指向的值,也會有所不同。this
代表的就是呼叫 function 的物件 (Owner Object of the function)。好,先來個例子吧,從大家最熟悉的物件講起:
var getGender = function(){
return people1.gender;
};
var people1 = {
gender: 'female',
getGender: getGender
};
var people2 = {
gender: 'male',
getGender: getGender
};
console.log( people1.getGender() );
console.log( people2.getGender() );
來,猜猜 console
後的結果是什麼?
沒錯,因為 getGender()
回傳寫死 people1.gender
的關係,結果當然是 'female'。
那麼,如果我們把 getGender
改一下:
var getGender = function(){
return this.gender;
};
這個時候,你應該會分別得到 female
與 male
兩種結果。
所以回到前面講的重點,從這個例子可以看出,即便 people1
與 people2
的 getGender
method 參照的都是同一個 getGender function,但由於呼叫的物件不同,所以執行的結果也會不同。
現在我們知道了第一個重點, this
會因執行的環境與上下文 (context) 的不同,而有不同的結果。
上面講過, this 代表的是 function 執行時所屬的物件。 而在 JavaScript 這個語言內,除了基本型別以外的一切都是「物件」。
那麼當 function
本身就是物件時,又如何呢?
var foo = function() {
this.count++;
};
foo.count = 0;
for( var i = 0; i < 5; i++ ) {
foo();
}
猜猜看,當這段程式碼執行後, foo.count
會是多少?
答案是 0
。 我知道你可能不能接受,來聽我解釋。
前面講過, this
代表的是 「function 執行時所屬的物件」對吧?
在上面範例中, foo
是 function,同時也是「全域變數」。 相信已經看到 DAY 20 這篇的你,一定很清楚「全域變數」的定義吧!
複習一下,「全域變數」代表的是「全域物件的屬性」。 所以說, foo
其實就是 window.foo
。
所以說,當 foo()
在 for
迴圈裡面跑得很開心的時候, this.count++
始終都是對 window.count
在做遞增的處理,因為這個時候的 this
實際上就是 window
。
而 window.count
理論上一開始會是 undefined
,在做了五次的 ++
之後,你會得到一個 NaN
的結果,而 foo.count
依然是個 0
。
記住, this
代表的是 function 執行時所屬的物件,而不是 function 本身。
再來一個範例:
var bar = function() {
console.log( this.a );
};
var foo = function() {
var a = 123;
this.bar();
};
foo();
相信經過前一個例題後,聰明的你應該知道 foo()
的執行結果應該是 undefined
了!
在這個範例中, foo()
可以透過 this.bar
取得 bar()
,是因為 this.bar
實際上是指向 window.bar
。
而 bar()
的 this.a
並非是 foo
中的 123
,而是指向 window.a
,所以會得到 undefined
的結果。
繼續來講一下很多人容易踩中的誤區,看範例:
var obj = {
func1: function(){
console.log( this === obj );
var func2 = function(){
// 這裡的 this 跟上層不同!
console.log( this === obj );
};
func2();
}
};
obj.func1();
在這個範例當中,會有兩次的 console
。
在 obj.func1()
裡面的 console.log( this === obj );
會印出 true
,原因是因為 func1
是透過 obj
來呼叫的。
但 obj.func1()
裡面的 func2()
在執行時的 console.log( this === obj );
卻會印出 false
。
這裡必須說明兩個重點:
function
。this
為 「全域物件」,也就是 window
。換言之,在 func2
裡頭的 this
,若是沒有特別透過 call()
、 apply()
或是 bind()
來指定 this
的話,那麼這裡的 this
就是 window
。
但要注意的是,在 ES5 的嚴格模式下,會禁止 this
自動指定為全域物件,像這樣:
var obj = {
func1: function(){
"use strict";
console.log( this === obj );
var func2 = function(){
// 宣告成嚴格模式後,這裡的 this 會變成 undefined。
console.log( this );
};
func2();
}
};
obj.func1();
假設我們今天在某個元素上透過 addEventListener
註冊了 click
事件,那麼根據先前在 重新認識 JavaScript: Day 15 隱藏在 "事件" 之中的秘密 曾經介紹過的,在事件中的 this
指的是「觸發事件的元素」。
然而,要是我們在事件的 callback function 加入 ajax 的請求,那麼根據前面所說的,預設綁定 (Default Binding) 會把這個 callback function 的 this
指定給 global object
,也就是 window
。
有個很簡單的方式可以解決這個問題,那就是透過另一個變數來對目前的 this
做參考:
el.addEventListener("click", function(event) {
// 透過 that 參考
var that = this;
console.log( this.textContent );
$ajax('[URL]', function(res) {
// this.textContent => undefined
console.log(that.textContent, res);
});
}, false);
像這樣,我們將事件內的 this
先用一個叫 that
的變數儲存它的參考,那麼在 ajax 的 callback function 就可以透過 that
來存取到原本事件中的 this
了。
透過另一個變數來暫存 this
的方式雖然方便,那麼有沒有其他方式可以取得原本 this
的內容呢? 在 JavaScript 有三個可以強制指定 this
的方式,分別是 call()
、 apply()
以及 bind()
。
.bind()
延續上個範例,我們先看 bind()
。 在前面範例中,我們用 that
這個變數來替代 this
,以便取得觸發 click
事件的元素。
如果用 bind()
改寫的話:
el.addEventListener("click", function(event) {
console.log( this.textContent );
// 透過 .bind(this) 來強制指定該 scope 的 this
$ajax('[URL]', function(res) {
console.log(this.textContent, res);
}.bind(this));
}, false);
像上面這樣,在 function
後面加上 .bind(this)
就可以強制將 ( )
內的物件帶入至 callback function 內。 於是 callback function 裡的 this
就會強制被指定成先前在 bind( )
裡面的內容了。
var obj = {
x: 123
};
var func = function () {
console.log(this.x);
};
func(); // undefined
func.bind(obj)(); // 123
加上了 bind
之後的 func.bind(obj)()
執行的結果,會替我們將 func
的 this
暫時指向我們所設定的 obj
。
於是,console.log(this.x)
的結果自然就是 obj.x
也就是 123
了。
這裡你可以想像成某個 function 在執行的時候,「暫時」把它掛在某個物件下,以便透過 this
去取得該物件的 Context。
實務上除了 ajax 的 callback function 以外,另外像是 setTimeout
、setInterval
這類的 function,也是常見需要特別處理 this
的場景。
this
值得一提的是,從 ES6 開始新增了一種叫做 「箭頭函式表示式」 (Arrow Function expression) 的函式表達式。
而箭頭函式有兩個重要的特性:
像這樣,我們可以直接在 ajax 的 callback function 中取用 this.textContent
:
el.addEventListener("click", function(event) {
console.log( this.textContent );
// 箭頭函式隱含「強制指定 this」 至 callback function 中
$ajax('[URL]', res => {
console.log(this.textContent, res);
});
}, false);
但要注意的是,無論是使用 'use strict'
或是再加上 .bind(xxx)
都無法改變 this
的內容,也不能作為物件建構子 (constructor)來使用。 箭頭函式方便歸方便,若是你的 function 內會有需要用到 this
的情況時,就需要特別小心你的 this
是不是在不知不覺中換了人來當。
.call()
與 .apply()
既然講到了強制指定 this
的方式,看完了 bind()
與「箭頭函式」,接下來就不能不講到 call()
與 apply()
。
假設今天有個 function 長這樣:
function func( ){
// do something
}
那麼我們可以透過 func()
來呼叫它。
當然你也可以用 .call()
或是 .apply()
來呼叫它:
func.call( );
func.apply( );
你可能會覺得奇怪,看起來沒什麼不同對吧,還要多打幾個字豈不是自找麻煩。 但如果遇上了需要帶參數的時候,就又顯得有些不同。
基本上 .call()
或是 .apply()
都是去呼叫執行這個 function ,並將這個 function 的 context 替換成第一個參數帶入的物件。 換句話說,就是強制指定某個物件作為該 function 執行時的 this
。
而 .call()
與 .apply()
的作用完全一樣,差別只在傳入參數的方式有所不同:
function func( arg1, arg2, ... ){
// do something
}
func.call( context, arg1, arg2, ... );
func.apply( context, [ arg1, arg2, ... ]);
.call()
傳入參數的方式是由「逗點」隔開,而 .apply()
則是傳入整個陣列作為參數,除此之外沒有明顯的差別。
bind()
讓這個 function 在呼叫前先綁定某個物件,使它不管怎麼被呼叫都能有固定的 this
。
尤其常用在像是 callback function 這種類型的場景,可以想像成是先綁定好 this,然後讓 function 在需要時才被呼叫的類型。
而 .call()
與 .apply()
則是使用在 context 較常變動的場景,依照呼叫時的需要帶入不同的物件作為該 function 的 this
。 在呼叫的當下就立即執行。
this
綁定的基本原則大致上可以分成下列四種:
第一種 預設綁定 (Default Binding) 我們前面已經介紹過了:
宣告在全域範疇 (global scope) 的變數,與同名的全域物件 (window 或 global) 的屬性是一樣的意思。
因為預設綁定的關係,當 function 是在普通、未經修飾的情況下被呼叫,也就是當 function 被呼叫的當下如果沒有值或是在 func.call(null)
或 func.call(undefined)
此類的情況下,此時裡面的 this
會自動指定至全域物件。
但若是加上 "use strict" 宣告成嚴格模式後,原本預設將 this
綁定至全域物件的行爲,會轉變成 undefined
。
隱含式綁定 (Implicit Binding) 指的是,即使 function 被宣告的地方是在 global scope 中,只要它成為某個物件的參考屬性 (reference property),在那個 function 被呼叫的當下,該 function 即被那個物件所包含。
function func() {
console.log( this.a );
}
var obj = {
a: 2,
foo: func
};
func(); // undefined
obj.foo(); // 2
在上面的範例中可以看到,根據 「預設綁定」的原則,直接呼叫 func()
的情況下,此時的 this.a
實際上會指向 window.a
,所以結果是 undefined
。
而當我們在 obj
物件中,將 foo
這個屬性指到 func()
的時候,再透過 obj
來呼叫 obj.foo()
的時候,雖然實際上仍是 func()
被呼叫, 但此時的 this
就會指向至 obj
這個 owner
的物件上,於是此時的 this.a
就會是 obj.a
也就是 2
。
理解了隱含式綁定的原則後,繼續來看看這個變化過的版本:
function func() {
console.log( this.a );
}
var obj = {
a: 2,
foo: func
};
obj.foo(); // 2
var func2 = obj.foo;
func2(); // ??
在這個版本中,我們宣告另一個變數 func2
指向 obj.foo
,那麼聰明的你是否可以猜到呼叫 func2()
的結果為何呢?
答案是 undefined
。
雖然 func2
看起來是對 obj.foo
的參考,但實際上 func2
參考的對象是 window.func
。
跟我們上回介紹的「範圍鏈」(Scope Chain) 不同的是,決定 this 的關鍵不在於它屬於哪個物件,而是在於 function「呼叫的時機點」,當你透過物件呼叫某個方法 (method) 的時候,此時 this 就是那個物件 (owner object)。
然後是顯式綁定 (Explicit Binding)。 相較於前兩種,顯式綁定就單純許多,簡單來說就是透過 .bind()
/ .call()
/ .apply()
這類直接指定 this 的 function 都可被歸類至顯式綁定的類型。
前面已介紹過不少範例,這裡就不再贅述。
最後一個是**「new」關鍵字**綁定。
在傳統類別導向 (class-oriented) 的程式語言中,建構子 (constructors) 是被附接到類別上的特殊方法,在透過 new
將 class
實體化的時候,這個建構子方法就會被呼叫。 而 JavaScript 雖然也有 new
這個關鍵字,運作時也與類別導向的語言行為類似,但由於 JavaScript 並不是一個類別導向的程式語言 (而是基於原型的物件導向) ,所以它的 new
運作原理並不相同。
當一個 function
前面帶有 new
被呼叫時,會發生:
this
綁定目標,也就是 this
會指向新建構的物件。new
產生的物件會被自動回傳。function foo(a) {
this.a = a;
}
var obj = new foo( 123 );
console.log( obj.a ); // 123
在上面的範例中,因為呼叫 foo
時,加了一個 new
,所以建構了一個新物件,並回傳到 obj
。
透過傳入的參數 123
,在建立物件的時候,會作為新物件的屬性 a
的值,這種用 new
建立 this
綁定的方式,就是 new
關鍵字綁定的方式。
綜合上述範例介紹,我們可以簡單總結出一個結論:
new
進行的嗎? 如果是,那 this
就是被建構出來的物件。.call()
或 .apply()
的方式呼叫的嗎? 或是 function 透過 .bind()
指定? 如果是,那 this
就是被指定的物件。this
就是那個物件。undefined
。那麼以上就是關於 this 的介紹。 相信在看完這篇之後,各位對於 this
在 JavaScript 這個程式語言所扮演的角色,應該會有更清楚、更深入的理解。
大大你好,關於一開始的
var bar = function(){
console.log(this.a);
};
var foo = function(){
var a = 123;
this.bar();
};
foo();
這段程式碼我在VSC上無法執行1, system表示bar並不是一個function,但如果我不透過this呼叫bar而是:
bar();
就可以正常執行,而且結果和您一樣是undefined.
我在想是我編譯器的定義問題嗎?
或者是已經在foo這個function裡面,所以當開始執行的時候,this.bar() 的this就會變成foo的物件而不是window了呢?
還請大大解惑謝謝!
啊,抱歉,應該按回應,結果按到另一個留言了 orz
請看下面留言
我猜想你應該是透過 VSC 的 Code Runner 來執行程式吧?
如果是的話,那麼在 foo(){ ... }
裡頭的 this
實際上會指到 node 的環境物件「global
」,這個環境物件基本上等同於瀏覽器的 「window
」。
那為什麼 this.bar()
會出錯,而 bar()
卻可以正確顯示呢?
回答這個問題前,你可以試著打開瀏覽器的 devtool 的 console 介面,並且執行上面程式碼。 我想應該會出現 undefined
的結果。
為什麼同一段程式,在瀏覽器執行與在 node 執行的結果會不同呢?
這是因為 JavaScript 在不同環境下,執行宿主的環境物件不同的原因所致。
上面說過,在瀏覽器下的全域物件 (或稱環境物件) 叫 window
。
而這個 window
有個很神奇的特性,就是它會將所有的「全域變數」都變成這個物件的「屬性」。
假設我們在瀏覽器下宣告一個全域變數: var a = 123;
這個時候你可以試著執行: console.log(window.a);
, 應該會得到 「123」 的結果。
但是在 node 環境下,你試著印出 global.a
,應該只會看到 undefined
。
回到問題,
var foo = function(){
var a = 123;
this.bar();
};
foo();
當我們在瀏覽器執行 foo()
時,實際上是執行 window.foo()
。
這樣想是不是就可以理解裡面的 this
是 window
還是 foo
了呢?
謝謝大大這下子觀念更清楚了!對的一開始都是用code runner來跑程式,後來有試了瀏覽器發現是行得通的,非常感謝你清楚又詳細的回覆
XD 我會繼續把30篇都看完
btw, 你的文章真的寫得很有趣,邏輯也很清楚,超讚
Kuro大大您好:
var name = '全域阿婆'
var auntie = {
name: '漂亮阿姨',
callName: () => {
console.log('1', this.name); // 1 全域阿婆
setTimeout(() => {
console.log('2', this.name); // 2 全域阿婆
}, 10);
},
}
auntie.callName();
想請教在callName屬性內使用arrow function並呼叫this.name為何取用到的this.name都是window object內的name而不是auntie object的name?原本以為會透過你文中所提arrow function會強制指定this至callbackfunction中,所以會跑出兩個漂亮阿姨?
麻煩了,感謝
mingyang
你好,文章中我曾提及:決定 this 的關鍵不在於它屬於哪個物件,而是在於 function「呼叫的時機點」
以你提供的範例來說,
var name = '全域阿婆'
var auntie = {
name: '漂亮阿姨',
callName: () => {
console.log('1', this.name); // 1 全域阿婆
setTimeout(() => {
console.log('2', this.name); // 2 全域阿婆
}, 10);
},
}
auntie.callName();
這個 this.name
在哪裡被定義的並不重要,重要的是看呼叫的當下這個 this
是誰。
所以問題來了,當執行 auntie.callName();
的時候,你覺得當下的 this
會是誰?
您好:
我自己認為第一個應該是漂亮阿姨(依照您所提的this所指向的不是function本身,而是function執行時所屬的物件),而在setTimeout中沒有特別指定的this也會指定給全域物件所以答案會是全域阿婆。
因此感到困惑,麻煩了
沒錯,你提到一個重點,「this所指向的不是function本身,而是function執行時所屬的物件」,這是在一般 function 的情況。
所以你可以試著改成這樣:
var name = '全域阿婆'
var auntie = {
name: '漂亮阿姨',
callName: function() {
console.log('1', this.name); // ??
},
}
auntie.callName();
這個時候 this.name
會是誰?
而改用 arrow function 之後的
var auntie = {
name: '漂亮阿姨',
callName: () => {
console.log('1', this.name); // ??
},
}
auntie.callName();
這個時候 this.name
又會是誰?
第一個的結果會是漂亮阿姨,因為呼叫物件方法時他所屬的物件是auntie,但第二個結果竟然是全域阿婆...,您有提到「this所指向的不是function本身,而是function執行時所屬的物件」是在一般的function情況下,所以arrow function的this都會指向全域嗎?
arrow function 的 this
要看在呼叫的當下那個「環境」的 this
是誰。
換個方式說,如果在 global 的環境下,你覺得這個 this
會是誰?
var auntie = {
name: '漂亮阿姨',
callName: () => {
console.log('1', this.name); // ??
},
}
// 這個 this 是誰?
console.log(this);
// 透過 arrow function 取得的 this 就是誰
auntie.callName();
在Global環境下的話this就是指向window了,真的很感謝您的回答,不知道arrow function是否有如您文中所提到的.bind()
,相關的方法,可以指定物件,感謝了。
arrow function 是無法使用 .bind()
來指定 this
的喔。
Kuro大,你好:
在 "this 不等於 function" 這段當中,
所以說,當 foo() 在 for 迴圈裡面跑得很開心的時候, this.count++ 始終都是對
windoo.count
在做遞增的處理,因為這個時候的 this 實際上就是 window 。而
windoo.count
理論上一開始會是 undefined ,在做了五次的 ++ 之後,你會得到一個 NaN 的結果,而 foo.count 依然是個 0 。
請問「windoo.count」是 window 還是 foo 的誤植,還是其它?
window.count
才對,是打錯字 XD